feat(objectql): publish SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE and isSystemWriteOrganizationRequiredError so consumers stop re-spelling the literal - #16156
Conversation
…sSystemWriteOrganizationRequiredError (#14936) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…iscriminating controls (#14936) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5569b1aa4d8a3589d0006db1ffbc150565deddf4 && git checkout 5569b1aa4d8a3589d0006db1ffbc150565deddf4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 94ecb7e5c4accdcab4c61e4c3525116f1a225f4f a4255ac68e7f418f7c71b098376bd38a5b603c4d && git checkout -B drift-repro 94ecb7e5c4accdcab4c61e4c3525116f1a225f4f && git merge --no-ff a4255ac68e7f418f7c71b098376bd38a5b603c4d
node scripts/docs-audit/affected-docs.mjs --json 94ecb7e5c4accdcab4c61e4c3525116f1a225f4f
|
Docs sweep — answering the Docs Drift Check's declared blind spotThe drift run reported 4 documentable anchors and one uncovered file: All pathspecs are plain directory forms. (The
The three hits, and why none is falsified
The advice question — the interesting one, and the answer is "no page, but a neighbouring one"⭐ Exactly one page teaches this convention, and it does so correctly —
That sentence is about
⇒ Nothing to change in Generated by Claude Code |
Contract review (clause ②) — PASS — PR #16156 at head
|
| # | claim | reading | verdict |
|---|---|---|---|
| 1 | Cross-realm instanceof is unsound (dual exports, two module copies); the class's own docblock mandates code; the missing piece is the affordance |
Card's measurement (A===B false, cross-realm instanceof false, code compare true) is the whole card; the fix ships the affordance and nothing else. | correct |
| 2 | Generality question answered with measurement, not built as a factory: 17/18 objectql error classes carry code (5 via named constant, 11 inline, 1 typeof); 14 bespoke recognizers repo-wide compare different things; a factory cannot remove the literal |
Reasoning holds — the constant is what removes the second spelling; the 11 inline classes are a follow-up sweep, filed not folded. | correct |
| 3 | Predicate returns boolean, deliberately not a type guard |
Right: a transport-rebuilt envelope carries the code without object/posture/reason; a guard would relocate the unsoundness. Pinned. |
correct |
| 4 | readonly code = SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE keeps the literal type (declaration byte-identical); the cross-package consumer typing SystemWriteOrganizationRequiredError['code'] keeps its drift protection; compile-time pin added |
Measured via the .d.ts ablation. |
correct |
| 5 | check:error-code-provenance sees the new *_CODE constdef as a [listed] stamp site under objectql's own owner key — no ledger edit, no waiver |
Run explicitly (the gate is not derived by dispatch-gates); the name shape is load-bearing and the docblock says so. |
correct |
| 6 | Collision with origin/claude/issue-13636-… avoided structurally; git merge-tree 0 conflicts |
Measured. | accepted |
| 7 | The #14935 fast-path waiver in packages/spec is left in place (removing it spans two other lanes) |
Correct fence; the waiver's own text names #14936 as its retirement trigger — the services/spec lanes close it. | accepted |
② semver
@objectstack/objectql minor — additive published exports, the five-precedent shape. Correct.
Evidence and landing
Checks on 032f9d3b: 12 success / 6 skipped / 16 in progress at review time; check-governed-merges --test on the 4 paths: 0 hits. Clearing, same stroke: needs:contract-review off #14936 and PR #16156 with provenance. Landing (ready + auto-merge SQUASH) from this seat once every check is green — at its next check-in if the dispatching seat has not.
Generated by Claude Code
…tant instead of spelling it (#14936) A lowercase error-code literal in a `code` position is an ADR-0112 D1 finding, and `check:error-code-casing` cannot tell a negative fixture from a real emission - it classified this site as `(emission)`. Deriving the value from SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE is not an opt-out: the gate's own output records that a code value with no literal at the position is out of reach for its patterns by construction. No `adr0112-ok:` suppression was added (the count stays 17) and KNOWN_LOWERCASE_CODES is untouched. It also makes the fixture track the constant rather than restate it - this card's own argument about consumers re-spelling literals, applied to its own test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Red
|
Contract review (clause ②) — PASS extended to head
|
Fixes #14936
Publishes the affordance that makes this class's own documented convention followable.
SystemWriteOrganizationRequiredErrorhas always said it is identified bycoderather thaninstanceof, "so the check survives crossing a package boundary where two copies of this module can exist". The convention was right; there was nothing to import for it, so a consumer's only sound option was to re-spell the literal.Two new exports from the package root, and nothing else changes:
SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE— the code as a value. The class field now reads from it, so exactly one spelling of the string remains in the package.isSystemWriteOrganizationRequiredError(err): boolean— the code compare itself, so a consumer never authors the string at all.⛔ Behaviour is untouched: same
code, same 500 status, same firing conditions, same #8844derive-or-refuseruling. Purely additive to what the package publishes.The generality question, answered with its measurement
The card asked whether other engine error classes carry the same "identified by
code, notinstanceof" convention, such that one recognizer factory would beat a bespoke predicate per error. The convention generalises. The factory does not follow from it, and I did not build one. Three measurements, all re-run here rather than recalled:1. Census of
packages/objectql/srcnon-test source. 18 classes extendError; 17 carry areadonly codefield — 5 via a named exported constant (the precedents triage named), 11 via an inline string literal, 1 via atypeofannotation. So the convention is real and broad.git grep … -- 'src/**/*.ts'returned zero hits for all five named precedents.git grepapplies no:(glob)magic, so**there behaves as a single*and silently excludessrc/*.ts— which is where all five actually live. A control on a known-present symbol fires 2 files under that pathspec and 4 under a plainsrcdirectory pathspec. Every count above is from the directory form.2. Repo-wide: 14 bespoke
is…Error-shaped recognizers across 8 packages, and ZERO recognizer factories. They are also not uniform in what they compare —isAuthzStoreUnavailableError(core) compares a brand key,isPermissionDeniedError(plugin-security) comparescodeornameor a message prefix,isUniqueViolationError/isMissingTableError(types) walk acausechain with a depth budget. A single factory overcodefits none of those three. What these predicates encode is per-error knowledge about how that error arrives, not a shared compare.3. A factory cannot remove the literal, which is this card's actual cost. Any factory's signature is
makeRecognizer(code), so every call site still supplies the code — the literal moves from thecatchto the factory call rather than disappearing. What removes it is the exported constant. The factory would address none of the three costs the card names.⇒ Shape built: the constant (the five-precedent convention, and the thing that actually removes the literal) plus one bespoke predicate on top of it (the card's named ask, and the 14-predicate convention).
Why the predicate returns
booleanand deliberately does NOT narrow toerr is SystemWriteOrganizationRequiredError: acodecompare is satisfied by any value carrying that code, including an envelope a transport rebuilt from the wire — #5437 withholds the prose and keeps the machine-readable code. A type guard would promiseobject,postureandreasonmembers such a value need not have, which moves the unsoundness one layer down instead of removing it. Pinned as a test.The remaining 11 classes still spelling their code inline are a follow-up sweep, filed separately rather than ridden here.
Clause-② — re-derived from this diff: yes
Instrument: build at head, swap both changed sources back to the merge base, rebuild, diff every declaration file
files[]publishes (files: ["dist", …]⇒ all six, not the two barrelsexportsnames), then classify each hunk. Restore proved byte-exact.dist/core.d.ts/dist/core.d.mtsdist/util-mkEsVIjj.d.ts/.d.mts(shared chunk, named by noexportsentry)dist/index.d.ts/dist/index.d.mtsClassifying the 57: all but three are JSDoc (not surface). The three declaration-text hunks are:
The root export list gains exactly those two names and loses none. No content-hashed chunk was renamed (file sets identical both snapshots), so none of that noise class applies. ⇒ Clause-②: yes — a widened published export surface,
minorchangeset attached,needs:contract-reviewon both carriers.⭐ Two corroborating readings worth keeping:
declare class SystemWriteOrganizationRequiredErroris byte-identical across the ablation,readonly code: "ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED"included. So moving the field to read from the constant did not widen it tostring— which matters, because the existing cross-package consumer types its own constant asSystemWriteOrganizationRequiredError['code']and would have kept compiling while silently losing its drift protection. Pinned as a compile-time test in a file carrying notest-typecheck-debt.jsonentry, so a future widening is red on arrival.check:api-surfaceis green and did not move — and that is correct here, not a failed measurement. Its own output names its subject: "@objectstack/spec public API surface + factory signatures unchanged", andbuild-api-surface.tsresolvesPKG_DIRtopackages/specand reads that package's ownexports. Its population ispackages/speconly, so an@objectstack/objectqlexport cannot move it by construction. (The general rule still holds: a greencheck:api-surfaceis never a Clause-②no.)Reconciling the
issue-13636branch collisionorigin/claude/issue-13636-orgless-write-declarationis still open —git branch -r --containslists only itself, so it is not inorigin/main. It does touch both of my files, as warned:tenancy/system-write-organization.ts— +12 lines inside the header docblock, after the Prime-Directive-12 paragraph at:67. My additions are the constant at:347and the predicate at the end of file.index.ts— its hunk edits theplatform-object-tenancy.jsexport block (:390-397in my numbering) and inserts a new block before:398. My name goes into the differentsystem-write-organization.jsblock at:375-384.Avoidance was structural: append at the file end and use a different export block, never edit the docblock or the block it edits. Measured rather than eyeballed —
git merge-tree --write-tree HEAD origin/claude/issue-13636-orgless-write-declarationexits 0: zero conflicts. Its only notes are regeneration reminders for three generated docs files that belong to that branch's diff and appear nowhere in mine.Verification — all at
032f9d3b1Exit codes captured by redirect-then-read, never through a pipe.
pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/system-write-organization.test.ts→ 34 passed. Reconciled so the green is not vacuous: 20 pre-existing (12 plainit+ 4it.each× 2) + 14 new (6 plain + one 8-rowit.each) = 34 exactly.pnpm --filter @objectstack/objectql typecheck→ 0, includingcheck:test-typecheck: "44 file(s) / 242 error(s)", exactly the ledger's recorded baseline ⇒ the new test file added zero type errors and stays unledgered.check:error-code-provenance(run explicitly — it is not derived bydispatch-gates, [finding] dispatch-gates --commands does not derive check:optional-error-sink for a diff that adds a new logger-sink interface #14880; the derivation places it in the "artifact rosters" block whose silence is a fact about a list, not a clearance) → 0. Proof it is about this change rather than a population excluding it, from--report:[listed] @objectstack/objectql → ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED (constdef) packages/objectql/src/tenancy/system-write-organization.ts:347. The new*_CODEconstant is a stamp site, it is seen, and it is[listed]under this package's own owner key — objectql already registers the code (System-context writes land untenanted at RUNTIME, so a single-tenant install keeps re-forking the autonumber scope and minting duplicate business identifiers — the producer #8686's backfill cannot reach (17.0.0 GA) #8844), so no ledger edit and no waiver is needed. The*_CODEname is load-bearing for exactly this reason and the docblock says so.dispatch-gates.mjs --repo objectstack-ai/objectstackderived 56 families from the real changeset (4 paths, three-dot semantics). Green here:check:dispatcher-error-vocabulary,nul-bytes,engine-double-contract,where-matcher,query-options-erasure,published-files,test-source-alias,type-source-resolution,cross-package-test-inputs,type-check-coverage,driver-memory-census,comment-mask-adoption,undeclared-dep-imports,platform-object-tenancy-census,changeset-gate-self-tests,objectui-changeset,check-empty-changeset,check-changeset-no-major,check-adr-0087-registration.check:system-context-census→ 0, run deliberately: this diff changes line counts inpackages/**, and a line-number-anchored census is exactly the family that a "my diff is small" narrowing misses.check:dts-closure→ 0, and its green covers this package: it swept 15 built packages, which is everydist/on disk,packages/objectql/distamong them.check:dual-build-cjs-loads— NOT MEASURED, not green. Exit 3 is the gate's ownPREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ … ⛔ This is NOT a pass: nothing was measured.It wants a whole-repopnpm build; CI runs it on this PR. Recorded as absent rather than banked.Declared narrowing: repo-wide sweeps (
pnpm lintover the whole tree, the fullpnpm buildthatdual-build-cjs-loadsneeds) were not run locally; CI runs the farm exactly once regardless. Everything above is the targeted half.Relationship to the fast path — the fast path landed FIRST
Measured on
origin/main, not inferred:PROVENANCE_WAIVERSinpackages/spec/src/api/error-code-ledger.zod.tsalready carries the@objectstack/plugin-sharing/ERR_SYSTEM_WRITE_ORGANIZATION_REQUIREDentry (adjudicated on #14937, maintainer ruling A, 2026-09-04), andplugin-sharing/src/sharing-rule-service.ts:137already declaresENGINE_ORGANIZATION_REFUSAL_CODE. So #14935's bookkeeping answer is in the tree ahead of this root-cause one.That waiver's own text says it is "removed together with the stamp site when #14936 lands and objectql publishes a recognizer". ⛔ This PR does not do that removal: it spans
packages/specandplugin-sharing, both outside this card's landing surface, and the waiver is held live three ways — removing the consumer's stamp site without removing the waiver reddens the gate in the other direction. Filed as follow-up work; #14935 is not addressed here and stays open for its lane to close.Generated by Claude Code